查看原文
其他

华为手机刷微博体验更好?技术角度的分析和思考

鸿洋 2021-10-13

The following article is from Android Performance Author Gracker

1背景


技术群里的小伙伴发了一条微博, https://weibo.com/1808884742/IApbpEVQr, 一位博主发现发现, Mate 30 Pro 有个很特别的现象(建议先去看一下视频)

这个视频描述和底下的猜测都不对,我这边总结一下这个现象:

微博这个 App 在华为的手机上,在主页列表上下滑动的情况下依然可以流畅加载图片,而同一个版本的微博客户端,安装到其他手机上,在主页列表上下滑动的情况下,则必须要等到滑动停止之后才会加载图片。

这个现象有什么特别呢 ?

  1. 从技术上讲,滑动列表停止后再加载图片是目前列表滑动优化中一个比较常见的优化项,很多主流 App 也都是这么做的 ,做这种处理主要是因为 如果在列表滑动的时候,碰到图片视频就加载,那么会加载很多无用的图片&&视频,浪费资源不说,还可能会影响真正用户看到的图片的加载速度 (加载一般都有并行上限和队列,队列里面无效的图片太多,后来的图片就得排队等待)。这里比较 特别的就是同一个版本的微博 APK,在华为的机型上与在其他机型上表现不一致,作为一个系统优化工程师,这个还是值得去搞清楚的(大胆猜测是微博针对华为的机型做了优化),那么这个优化的内容是什么?

  2. 从用户体验的角度来讲,列表滑动的同时加载图片,用户可以更早地看到图片,减少图片占位白图的显示时间,可以提升滑动的体验

  3. 第三个现象就得认真体验才会感觉到:华为手机上的微博在松手后的滑动曲线和其他手机上的微博在松手后的滑动曲线是不一样的,华为的微博列表松手后的滑动曲线速度更慢,更柔和,结束的时候也不会太突兀,与系统默认的列表滑动曲线明显不一样


上面三个是从现象上来说的,下面就从技术上来验证,从最后的结果来看,华为和微博的合作毫无疑问是很成功的,可以作为一个案例推广到其他头部 App,同时作为 Android 开发者,对华为这种非常细致的体验优化真的是非常敬佩。

2背景备注


  1. 由于 “列表滑动的同时加载图片” 这个功能由微博官方服务器控制,可以随时开启或者关闭,所以文章中所说的 “同一个版本的微博客户端,安装到其他手机上,在主页列表上下滑动的情况下,则必须要等到滑动停止之后才会加载图片” 这个现象在 “列表滑动的同时加载图片” 这个功能开启后,现象就会变成 “主页列表上下滑动的时候就会加载图片”

  2. 在 2020-6 月左右分析这个问题的时候,“列表滑动的同时加载图片” 这个功能还是关闭的,只有华为手机做了优化才有效果,其他手机是 “滑动停止之后才会加载图片”

  3. 在 2020-8 月再看这个问题的时候,“列表滑动的同时加载图片” 这个功能在其他手机上已经开启

  4. 华为的 PerfSDK 还有效果么?答案是有,具体分析可以看下文,因为有了这个 SDK,不仅对微博有好处(减少图片加载个数),对华为也有好处(提升微博主页列表在华为手机上的滑动体验,即 Fling 曲线优化) ;而粗暴开启 “列表滑动的同时加载图片” 的其他手机,如果性能不足,开启后反而会增加卡顿出现的概率(微博官方应该有性能监控数据可以看到)

  5. 反编译的微博版本:10.8.1


3结论先行


‘’微博这个 App 在华为的手机上,在主页列表上下滑动的情况下依然可以流畅加载图片 ‘’这个现象是因为华为和微博做了联合优化,主要是为了优化微博列表滑动时候的用户体验,其优化点如下

  1. 华为提供了一个简单的接口打包成 SDK 提供给微博,这个接口可以让微博的列表监听到列表的当前速度(Velocity),在速度高于阈值或者低于阈值的时候,都会及时通知 App

  2. 微博拿到这个速度回调之后,就可以根据列表的滑动速度来决定是否要在滑动过程中加载图片,一旦列表的滑动速度低于设定的阈值,就开启图片加载;一旦列表的滑动速度高于设定的阈值,就关闭图片加载

  3. 华为检测到这个应用使用了 SDK,就可以将优化过后的滑动曲线应用在这个 App 的列表 Fling 阶段,提升用户体验


对细节感兴趣的同学可以继续阅读,有能力的同学看完后可以修改 Framework 相关代码,编译一个 SDK,然后自己写个 Demo 接入 SDK,就可以打通我下面所说的所有内容了,我自己在 AOSP 的代码上实现了一遍,Demo 也可以正常运行,有兴趣可以跟我私下交流。

4微博+华为是怎么优化?


现象分析


我们在滑动微博列表的时候,一个滑动操作主要由下面三部分组成

  1. 手指接触屏幕,上下滑动微博主页列表,但是手指 没有离开屏幕 ,这个阶段我们称之为阶段一,技术术语为 SCROLL_STATE_TOUCH_SCROLL

  2. 手指上下滑动的时候 离开屏幕 (必须有一个上滑或者下滑的速度),微博列表有了一个惯性,根据惯性的方向继续滑动,这个阶段我们称之为阶段二,技术术语为 SCROLL_STATE_FLING

  3. 列表惯性滑动后停止,这 个 阶段我们称之为阶段三 , 技术术语为  SCROLL_STATE_I DLE


而华为和微博的优化主要在阶段一和阶段二。

阶段一优化


  1. 优化前:只要手指不离开屏幕,图片加载功能关闭
  2. 优化后:只要手指不离开屏幕,列表就不会滑动太快,这时候图片加载功能开启


阶段二优化


  1. 滑动图片加载优化
    1. 列表滑动速度太快,这时候图片加载功能关闭
    2. 列表滑动速度掉落到一个阈值,图片加载功能开启
    3. 优化前:只要列表滑动不停止,图片加载功能关闭
    4. 优化后:图片加载功能是否开启取决于当前列表滑动的速度
  2. 列表 Fling 曲线优化
    1. 优化前:列表滑动的曲线是默认值,滑动时间比较短,停止的时候比较突兀,不柔和
    2. 优化后:列表滑动的曲线是华为经过优化的,滑动时间比较长,停止的时候比较柔,不突兀,比较接近 iPhone 的列表滑动曲线


技术分析


技术分析的代码主要来源于微博 apk 的反编译,微博版本 10.8.1,通过反编译的代码可以看到, 微博主页在初始化的时候,会接入华为提供的 PerfSDK,从而获得监听列表滑动速度的能力。

阶段一优化的技术分析


列表的 ScrollStateChange 是标识列表状态的一个回调,微博在 ScrollStateChange 这个回调中会根据当前的状态来决定是否加载图片, 从下面的代码逻辑来看:

  1. 当 滑动图片加载优化生效 的时候,如果 State != 2,那么就允许 ImageLoader 加载图片,State 为 2 也就是 SCROLL_STATE_FLING,熟悉列表滑动的同学应该知道,SCROLL_STATE_FLING  就是 滑动列表的时候手指松手后列表继续滑动的那一段 ,叫 fling,毕竟只有 fling 的时候才有 Velocity,松手后会根据这个值的大小计算滑动曲线和滑动时长

  2. 当 滑动图片加载优化不生效 的时候,就到了常规的列表滑动优化:即列表停止之后才开始加载图片 :State !=0,0 即 SCROLL_STATE_IDLE



阶段二优化的技术分析


微博的主页在初始化的时候,会给首页的 ListView 注册一个 HwPerfVelocityCallback,从名字可以看出来,这个回调是监听 Velocity 的,也就是滑动的速度,两个回调:

  1. HwPerfonVelocityDownToThreshold : 当速度降低到阈值之后,打开 ImageLoader 的图片加载功能

  2. HwPerfonVelocityUpToThreshold:当速度升高到阈值之后,关闭 ImageLoader 的图片加载功能


下图为反编译后的源码


至于滑动曲线,则需要查看华为的 Framework 的代码,由于代码量比较大,这里只贴一下 OverScroller.java 中的 update 方法,具体感兴趣的可以自己去翻一番华为的 Framework 代码:


计算 Distance 的代码


计算 Velocity 的代码


关于滑动曲线的解释,大家可以看这一篇知乎回答,其中对比了 iOS 和 Android 的滑动曲线的不同 :为什么 iOS 的过渡动画看起来很舒服?
https://www.zhihu.com/question/291779390/answer/484881732

其他厂商处理


上面图中代码最后一段还有一个判断开关, 如果 boolean a = HwPerfUtil.m14290a 这个返回的是 false,这就是说有可能华为这个优化关闭了,有可能是非华为机器,那么会 判断 Android 版本号和全局 Feature 开关:


对应的 FeedAbManager 就是一个 Feature 管理器,可以在线开关某些 Feature


而 m52580k 的实现如下


可以看到这里还受到一个全局的 Feature 配置:feed_scroll_loadimage_enable,这个 Feature 是服务端可以配置的


这里就是处理其他厂商的逻辑。

最后一个问题:滑动点击


滑动点击是个什么问题呢?列表在滑动的过程中,如果用户点击列表的某一个 Item,那么根据 Android 的事件分发机制,这时候列表的 Item 并不会被点击到,而是整个列表先收到点击事件,然后 触发列表滑动停止;列表停止的时候点击 Item 才会触发 Item 的点击。

上面阶段二的优化中,在优化了滑动曲线之后,列表处于 Fling 状态的时间变长,如果用户想点击去某一个 Item 查看详情,那么需要先点击一下让列表停止,然后再点击一下才能进去,这就是这一节想说的 :滑动点击问题。

滑动(Fling 状态)和点击其实是需要一个平衡的,这个平衡需要开发者自己去把控:

滑动(Fling 状态)的时间越短,列表越容易停下,用户点击列表越容易触发 Item 的点击,但是容易停止带来的问题就是不够柔和。想象你在粗糙的水泥地上滑出去一块石头,这石头没有滑动多久就会停止,不管是扔石头的你还是旁边看你扔石头的我,都不会觉得这有什么美感,但是没得选。这个的 代表其实就是 Android 原生的 Fling 曲线。

滑动(Fling 状态)的时间越长,滑动(Fling 状态)的时间越长,列表越不容易停下,用户点击列表越不容易触发 Item 的点击,如果曲线优化的好,给人的感觉就是很柔和,符合物理规律,想象你在光滑的冰面上滑出去一块冰,冰面越滑,冰块滑动的时间就越长,越不容易停下。这其中的极端代表就是 iOS 的 Fling 曲线。

说 iOS 极端是因为,iOS 的滑动曲线调的太磨叽了,时间长不说,停的异常慢,很多时候你都需要点击一下列表让他先停止,然后再进行下一步的点击动作。而小米的 MIUI12 对这个也进行了调整,效果要比 iOS 好一些,如果再和三方进行类似华为和微博的合作,体验会更上一层楼。

滑动点击问题其实也可以通过厂商和 App 合作来解决,比如,当滑动到整个滑动距离的 98%(或者 95%) 之后,用户点击列表不再是让列表停止,而是列表内的 item 响应这个点击。

这个思想来源于 Launcher 的代码,Launcher 的每一页在左右滑动的时候,如果滑动还没有停止但是用户比较手速快点击了某个 icon 想启动,那么这时候不会触发 Page 停止,而是直接响应 icon 的点击启动应用操作。

5延伸阅读


列表滑动图片加载的性能考虑


前文有提到这个问题,滑动的时候进行图片加载主要有两个问题:

  1. 如果用户滑动非常快,比如是想找昨天发的某个微博,那么今天发的所有的带图片的微博在用户滑动的时候是没必要加载的,因为用户的目标不是这些图片,而 App 去加载这些图片,而程序员是不会为用户提前加载你未看到的数据,因为加载过多的数据不仅容易发生数据复用、缓存过多、内存溢出等错误,还会对服务器造成不必要的资源请求。

  2. 如果用户滑动非常快,那么图片加载队列势必有许多无效的资源(对这一刻的用户来说),而用户真正想看的图片反而排在了加载队列后面,造成加载速度变慢,也会影响用户的体验


滑动中加载图片最大的风险其实就是造成卡顿,因为图片加载本身就是一个比较重的操作,而高帧率的手机上,一帧的时间被压缩到很短,任何小的不确定性都有可能造成卡顿。

所以厂商+应用的这个优化:快速滑动不加载图片,慢速的时候再加载,然后优化滑动曲线 ,其实对厂商和应用都是非常有益处的、

列表滑动监听背景知识


下面的 AbsListView 的 OnScrollListener 里面标注了列表滑动的三个状态

  1. 滑动停止:SCROLL_STATE_IDLE
  2. 手指在屏幕上滑动:SCROLL_STATE_TOUCH_SCROLL
  3. 手指离开屏幕,列表靠惯性继续滑动:SCROLL_STATE_FLING

两个回调

  1. 列表状态变化时的回调 :onScrollStateChanged
  2. 列表滑动时候的回调:onScroll

public interface OnScrollListener { 

    // The view is not scrolling. Note navigating the list using the trackball counts as being in the idle state since these transitions are not animated. 
    public static int SCROLL_STATE_IDLE = 0;      

    //The user is scrolling using touch, and their finger is still on the screen 
    public static int SCROLL_STATE_TOUCH_SCROLL = 1;      

    //The user had previously been scrolling using touch and had performed a fling. The animation is now coasting to a stop 
    public static int SCROLL_STATE_FLING = 2;      

    // Callback method to be invoked while the list view or grid view is being scrolled. If the view is being scrolled, this method will be called before the next frame of the scroll is rendered. In particular, it will be called before any calls to 
    public void onScrollStateChanged(AbsListView view, int scrollState);      

    // Callback method to be invoked when the list or grid has been scrolled. This will be called after the scroll has completed 
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
}


列表滑动状态的变化


TOUCH_SCROLL、FLING、IDLE 三个状态对应的列表滑动操作如下

  1. TOUCH_SCROLL:手指滑动 List 阶段,但是手指没有离开屏幕,这时候上下滑动都是 TOUCH_SCROLL
  2. FLING:手指滑动 List 后抬手到 List 停止的阶段(必须有一个上滑或者下滑的速度,否则不会进入 Fling)
  3. IDLE:List 停止阶段

这三个状态的变化情况如下

  1. 手指滑动列表,停止后松手:IDLE  -> TOUCH_SCROLL -> IDLE
  2. 手指滑动列表,松手后列表继续滑动,然后停止:IDLE  -> TOUCH_SCROLL -> FLING -> IDLE


列表的 Fling 曲线计算


Fling 触发之后,每一帧都会调用 update 函数来更新 distance  和 mCurrVelocity,所以我们只需要监听 mCurrVelocity  的值,超过一定的阈值,就可以回调给 App
frameworks/base/core/java/android/widget/OverScroller.java

boolean update() 
    final long time = AnimationUtils.currentAnimationTimeMillis(); 
    final long currentTime = time - mStartTime; 
    double distance = 0.0
    switch (mState) { 
        case SPLINE: { // Fling  状态 
            final float t = (float) currentTime / mSplineDuration; 
            final int index = (int) (NB_SAMPLES * t); 
            float distanceCoef = 1.f; 
            float velocityCoef = 0.f; 
            if (index < NB_SAMPLES) { 
                final float t_inf = (float) index / NB_SAMPLES; 
                final float t_sup = (float) (index + 1) / NB_SAMPLES; 
                final float d_inf = SPLINE_POSITION[index]; 
                final float d_sup = SPLINE_POSITION[index + 1]; 
                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); 
                distanceCoef = d_inf + (t - t_inf) * velocityCoef; 
            } 
            distance = distanceCoef * mSplineDistance; 
            mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f
            break
        } 
        case BALLISTIC: { // 列表滑到 底 之后的 拉伸阶段 
            final float t = currentTime / 1000.0f
            mCurrVelocity = mVelocity + mDeceleration * t; 
            distance = mVelocity * t + mDeceleration * t * t / 2.0f
            break
        } 
        case CUBIC: { // 列表滑到底拉伸 之后的 回弹阶段 
            final float t = (float) (currentTime) / mDuration; 
            final float t2 = t * t; 
            final float sign = Math.signum(mVelocity); 
            distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);  
            mCurrVelocity = sign * mOver * 6.0f * (- t + t2);  
            break
        } 
    } 
    mCurrentPosition = mStart + (int) Math.round(distance); 
    return true
}

厂商应用联合优化


微博这个优化就是厂商和应用之间联合优化的一个案例,应用对用户体验的极致追求,让这种合作在未来会变得更加频繁,像微信、快手、抖音这些...




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!




推荐阅读

反思系列:深入探索ANR机制的设计与实现
实际生产中的 Android Lint实践分享
分享几个“装逼”知识点



扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存